البرمجة

المتغيرات الشرطية في لغة C

جدول المحتوى

الفصل العاشر: المتغيرات الشرطية وحل مشاكل التزامن بين العمليات في لغة C

تُعد المتغيرات الشرطية (Condition Variables) من الأدوات الحيوية والأساسية في برمجة الأنظمة متعددة العمليات أو متعددة الخيوط (Multithreading) في لغة C، حيث تلعب دورًا جوهريًا في التحكم بالتزامن بين العمليات أو الخيوط، وحل المشاكل التي تنشأ عند مشاركة الموارد. هذا الفصل يستعرض بالتفصيل مفهوم المتغيرات الشرطية، آلية عملها، دورها في حل مشاكل التزامن بين العمليات في لغة C، بالإضافة إلى شرح مفصل لمجموعة من الأمثلة والآليات التي تدعمها المكتبة القياسية POSIX Threads (pthreads).


مفهوم التزامن بين العمليات

في بيئات البرمجة التي تعتمد على تنفيذ أكثر من عملية أو خيط في نفس الوقت، تظهر الحاجة إلى تنسيق هذه العمليات بشكل يضمن عدم حدوث تعارض أو فساد في البيانات المشتركة. التزامن (Synchronization) هو العملية التي تهدف إلى ترتيب تنفيذ هذه العمليات بحيث لا تتداخل في الموارد المشتركة بطريقة غير منظمة تؤدي إلى أخطاء أو نتائج غير متوقعة.

مشكلة التزامن بين العمليات

تحدث مشاكل التزامن عادة عندما يحاول أكثر من خيط أو عملية الوصول إلى نفس المورد أو المتغير المشترك في نفس اللحظة دون التنسيق اللازم. من هذه المشاكل:

  • التداخل (Race Condition): حيث يتنافس خيطان أو أكثر على قراءة أو كتابة نفس المتغير أو المورد في نفس الوقت، مما يؤدي إلى نتائج غير صحيحة.

  • الجمود (Deadlock): حيث يتم حجز الموارد بين عمليات أو خيوط مختلفة بحيث ينتظر كل منها الآخر ولا يمكن لأي منها التقدم.

  • الجوع (Starvation): حيث تبقى بعض العمليات تنتظر وقتًا طويلاً للحصول على الموارد بسبب إعطاء أولوية عالية لعمليات أخرى.


الأدوات الأساسية لحل مشاكل التزامن في C

لغة C وحدها لا توفر آليات مدمجة مباشرة للتحكم في التزامن، لكنها تعتمد على مكتبات نظام التشغيل، وأشهرها مكتبة POSIX Threads (pthreads)، التي توفر أدوات متقدمة مثل:

  • المقاطع الحرجة (Critical Sections): للتحكم في دخول الخيوط إلى الموارد المشتركة.

  • القفل (Mutex): لتأمين الموارد بحيث لا يدخلها أكثر من خيط في نفس الوقت.

  • المتغيرات الشرطية (Condition Variables): لتنظيم انتظار الخيوط لظروف معينة قبل الاستمرار في التنفيذ.


تعريف المتغير الشرطي

المتغير الشرطي هو آلية تتيح لعملية أو خيط الانتظار حتى يتحقق شرط معين قبل الاستمرار في تنفيذ الكود. وهو يمثل نوعًا من التزامن يعتمد على إشارات (signals) بين العمليات، حيث يمكن لعملية أن تنتظر إشارة أو شرط معين لتغيير الحالة.

المتغير الشرطي لا يضمن التزامن بمفرده، وإنما يعمل جنبًا إلى جنب مع المقفل (Mutex) الذي يحمي حالة المتغيرات المشتركة.


كيفية عمل المتغير الشرطي

  1. الانتظار (Wait): يقوم الخيط بدخول حالة انتظار عبر pthread_cond_wait()، حيث يتم إيقاف تنفيذ الخيط مؤقتًا حتى يصدر متغير شرطي إشارة بالإفاقة.

  2. الإشارة (Signal): عندما يحدث تغيير في الحالة المرتبطة بالشرط، يقوم خيط آخر بإرسال إشارة باستخدام pthread_cond_signal() أو pthread_cond_broadcast() لإيقاظ خيط أو جميع الخيوط المنتظرة.

  3. استئناف التنفيذ: بعد استقبال الإشارة، يستأنف الخيط تنفيذ الكود بعد عملية الانتظار.


الفرق بين pthread_cond_signal() و pthread_cond_broadcast()

  • pthread_cond_signal() يقوم بإيقاظ خيط واحد فقط من الخيوط المنتظرة على نفس المتغير الشرطي.

  • pthread_cond_broadcast() يقوم بإيقاظ جميع الخيوط المنتظرة على نفس المتغير الشرطي.

يُستخدم signal عندما يكون هناك خيط واحد فقط يحتاج إلى الاستيقاظ، بينما broadcast مناسبة عندما يكون هناك أكثر من خيط ينتظر نفس الشرط ويجب إيقاظهم جميعًا.


استخدام المتغيرات الشرطية لحل مشاكل التزامن

في حالات كثيرة، لا يكفي فقط حماية المتغيرات المشتركة بقفل Mutex، بل يحتاج الخيط إلى انتظار حالة معينة أو تحقق شرط معين للمتابعة، مثلاً:

  • انتظار أن يتوفر عنصر في قائمة أو طابور.

  • انتظار أن تكون البيانات جاهزة للقراءة.

  • انتظار أن يصبح المورد غير مشغول.

هنا يأتي دور المتغيرات الشرطية التي تتيح تعلّق الخيط بانتظار تحقق الشرط دون إهدار موارد المعالج، بخلاف الانتظار النشط (Busy Waiting) الذي يستهلك الوقت والمعالج بدون فائدة.


خطوات استخدام المتغير الشرطي في C

1. تهيئة المتغير الشرطي والمقفل

c
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER; pthread_cond_t cond = PTHREAD_COND_INITIALIZER;

2. الدخول إلى القسم الحرج باستخدام القفل

c
pthread_mutex_lock(&mutex);

3. انتظار تحقق الشرط باستخدام pthread_cond_wait()

c
while (!condition) { pthread_cond_wait(&cond, &mutex); }

4. تنفيذ الكود بعد تحقق الشرط

5. تحرير القفل

c
pthread_mutex_unlock(&mutex);

6. عند تغيير حالة الشرط، يجب إرسال إشارة

c
pthread_mutex_lock(&mutex); // تغيير حالة الشرط هنا pthread_cond_signal(&cond); pthread_mutex_unlock(&mutex);

مثال عملي: نموذج المنتج-المستهلك (Producer-Consumer Problem)

تعد مشكلة المنتج-المستهلك من أشهر مشاكل التزامن، حيث يقوم خيط المنتج بإضافة عناصر إلى مخزن مشترك، في حين يقوم خيط المستهلك بسحب هذه العناصر، ويجب ضمان عدم سحب عنصر غير موجود وعدم إضافة عناصر عند امتلاء المخزن.

المتغيرات المستخدمة:

  • مخزن مؤقت (Buffer): مكان تخزين مؤقت للعناصر.

  • مؤشرات إدخال وإخراج (In/Out): لتحديد مواضع الإضافة والإزالة.

  • عداد العناصر (Count): لتتبع عدد العناصر في المخزن.

  • قفل Mutex: لحماية المخزن المؤقت.

  • متغير شرطي للإشارة إلى المخزن غير الفارغ (not_empty).

  • متغير شرطي للإشارة إلى المخزن غير الممتلئ (not_full).

الشيفرة:

c
#define MAX 10 int buffer[MAX]; int count = 0, in = 0, out = 0; pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER; pthread_cond_t not_empty = PTHREAD_COND_INITIALIZER; pthread_cond_t not_full = PTHREAD_COND_INITIALIZER; void *producer(void *arg) { int item; while (1) { item = produce_item(); // دالة توليد عنصر جديد pthread_mutex_lock(&mutex); while (count == MAX) { pthread_cond_wait(¬_full, &mutex); } buffer[in] = item; in = (in + 1) % MAX; count++; pthread_cond_signal(¬_empty); pthread_mutex_unlock(&mutex); } } void *consumer(void *arg) { int item; while (1) { pthread_mutex_lock(&mutex); while (count == 0) { pthread_cond_wait(¬_empty, &mutex); } item = buffer[out]; out = (out + 1) % MAX; count--; pthread_cond_signal(¬_full); pthread_mutex_unlock(&mutex); consume_item(item); // دالة استهلاك العنصر } }

شرح العمل:

  • المنتج ينتظر إذا كان المخزن ممتلئًا (count == MAX) باستخدام المتغير الشرطي not_full.

  • المستهلك ينتظر إذا كان المخزن فارغًا (count == 0) باستخدام المتغير الشرطي not_empty.

  • عندما يضيف المنتج عنصرًا، يُنبه المستهلك أن المخزن لم يعد فارغًا.

  • وعندما يستهلك المستهلك عنصرًا، يُنبه المنتج أن المخزن لم يعد ممتلئًا.


مزايا استخدام المتغيرات الشرطية

  • كفاءة استخدام المعالج: مقارنة بالانتظار النشط، حيث يتم تعليق الخيط حتى يستيقظ بالإشارة، مما يقلل استهلاك الموارد.

  • تنظيم التزامن بشكل أكثر دقة: حيث تتيح الانتظار على شروط محددة وليس فقط تأمين الوصول للموارد.

  • دعم متعدد الخيوط: يمكن استخدام متغير شرطي واحد لإيقاظ خيط أو أكثر حسب الحاجة.


اعتبارات ومشاكل شائعة في استخدام المتغيرات الشرطية

  • المتغير الشرطي لا يحمي البيانات بمفرده: يجب استخدامه مع قفل Mutex دائمًا، حيث يجب أن تكون الحالة المشتركة محمية عند التحقق من الشرط أو تعديله.

  • استخدام حلقة عند الانتظار: من الضروري دائمًا لف التحقق من الشرط داخل حلقة while وليس شرطًا عاديًا if لأن الخيط قد يستيقظ دون تحقق الشرط (Spurious Wakeups).

  • التأكد من الترتيب الصحيح للإشارات: إرسال الإشارة يجب أن يتم فقط بعد تعديل حالة الشرط.

  • التزامن بين الإشارات والانتظار: يجب الحذر لتجنب حالات السباق (Race Conditions) عند إرسال الإشارات.


علاقة المتغيرات الشرطية بخوارزميات التزامن الأخرى

المتغيرات الشرطية ليست بديلاً للقفل (Mutex) أو السيمي فور (Semaphore)، لكنها مكملة لها، وتستخدم لتحقيق تزامن متقدم قائم على انتظار تحقق شروط معينة.

الفرق بين المتغيرات الشرطية والسيمي فور

  • السيمي فور (Semaphore): هو عداد يتحكم بعدد العمليات التي يمكنها الوصول إلى المورد، ويوفر إمكانية الوصول المتعدد.

  • المتغير الشرطي: يستخدم لإيقاف خيط حتى تتحقق حالة معينة، ولا يملك عداد داخلي، ويعتمد على القفل لضمان حماية الحالة المشتركة.


تطبيقات عملية أخرى للمتغيرات الشرطية في لغة C

  • انتظار انتهاء مهمة: مثل انتظار اكتمال تحميل ملف أو الانتهاء من حساب معين قبل متابعة التنفيذ.

  • تنظيم عمليات الدخول إلى مناطق حرجة مع شروط متغيرة.

  • تطبيق أنظمة الانتظار في البرمجة الشبكية أو أنظمة قواعد البيانات.

  • التزامن في برامج واجهات المستخدم الرسومية (GUI) عند انتظار أحداث معينة.


الجدول التالي يوضح الفروقات والخصائص الرئيسية بين الأدوات المختلفة للتزامن في C

الأداة وظيفة رئيسية تدعم الإيقاف والانتظار تحتاج إلى قفل (Mutex) تستخدم لانتظار شرط محدد يمكنها التحكم في عدد الخيوط المتزامنة
Mutex تأمين الوصول الحصري لمورد لا لا (هي نفسها القفل) لا لا
Semaphore عداد للتحكم في عدد العمليات نعم لا لا نعم
Condition Variable انتظار تحقق شرط معين قبل المتابعة نعم نعم نعم لا (توقيف/تنبيه الخيوط حسب شرط معين)

استنتاجات حول المتغيرات الشرطية ودورها في لغة C

المتغيرات الشرطية تمثل حجر الزاوية في إدارة التزامن المتقدم في أنظمة وبرمجيات متعددة الخيوط في لغة C. تكمن أهميتها في قدرتها على ربط عمليات الانتظار المنطقية بشروط محددة، مما يوفر بيئة برمجية أكثر أمانًا وكفاءة. إن دمجها مع المقفل Mutex يشكل آلية متينة للحماية والتنسيق بين الخيوط، مما يمنع المشاكل الشائعة مثل التداخل والجمود.

يحتاج المبرمج إلى فهم دقيق لطريقة عمل المتغيرات الشرطية، والاعتبارات الخاصة بها، وكيفية تطبيقها في سيناريوهات الحياة الواقعية مثل نماذج المنتج-المستهلك، وأنظمة إدارة الموارد المشتركة. كما أن الانتباه إلى التفاصيل الدقيقة في كتابة الأكواد باستخدام pthread_cond_wait وpthread_cond_signal يمكن أن يضمن أن البرامج تعمل بكفاءة وبدون أخطاء تعقيدية.


المراجع

  1. موقع POSIX Threads Programming

    https://man7.org/linux/man-pages/man7/pthreads.7.html

    شرح رسمي ومفصل لدوال مكتبة pthreads مع أمثلة.

  2. كتاب “Programming with POSIX Threads” – David R. Butenhof

    مرجع شامل عن برمجة الخيوط ومزامنتها باستخدام مكتبة POSIX Threads.


بهذا يكون المقال قد تناول المتغيرات الشرطية في لغة C من النواحي النظرية والتطبيقية، مع توضيح دورها الحيوي في حل مشاكل التزامن بين العمليات، وهو مناسب لتغطية متطلبات المشاريع والأنظمة متعددة الخيوط.